package org.codefilarete.stalactite.engine;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;

import org.codefilarete.reflection.AccessorByMethodReference;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.stalactite.engine.EntityCriteria.CriteriaPath;
import org.codefilarete.stalactite.engine.EntityCriteria.FluentOrderByClause;
import org.codefilarete.stalactite.engine.EntityCriteria.SerializableCollectionFunction;
import org.codefilarete.stalactite.engine.listener.PersisterListener;
import org.codefilarete.stalactite.mapping.SimpleIdMapping;
import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
import org.codefilarete.stalactite.query.model.ConditionalOperator;
import org.codefilarete.stalactite.query.model.JoinLink;
import org.codefilarete.stalactite.query.model.Select;
import org.codefilarete.stalactite.query.model.Selectable;
import org.codefilarete.stalactite.sql.result.Accumulator;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.Experimental;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.KeepOrderMap;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.collection.Maps;
import org.codefilarete.tool.collection.PairIterator;
import org.danekja.java.util.function.serializable.SerializableBiConsumer;
import org.danekja.java.util.function.serializable.SerializableFunction;

/**
 * @author Guillaume Mary
 */
public interface EntityPersister<C, I> extends PersistExecutor<C>, InsertExecutor<C>, UpdateExecutor<C>, SelectExecutor<C, I>, DeleteExecutor<C, I>, PersisterListener<C, I> {
	
	/**
	 * Persists an instance either it is already persisted or not (insert or update).
	 *
	 * Check between insert or update is determined by id state which itself depends on identifier policy,
	 * see {@link SimpleIdMapping#IsNewDeterminer} implementations and
	 * {@link IdentifierInsertionManager} implementations for id value computation. 
	 *
	 * @param entity an entity to be persisted
	 * @throws StaleStateObjectException if updated row count differs from entities count
	 * @see #insert(Iterable)
	 * @see #update(Iterable, boolean)
	 */
	default void persist(C entity) {
		// determine insert or update operation
		persist(Collections.singleton(entity));
	}

	/**
	 * Choose either to insert or update entities according to their persistent state.
	 * 
	 * @param entities entities to be inserted or updated according to {@link #isNew(Object)} result
	 */
	void persist(Iterable<? extends C> entities);
	
	default void insert(C entity) {
		insert(Collections.singletonList(entity));
	}
	
	/**
	 * Updates an instance that may have changes.
	 * Groups statements to benefit from JDBC batch. Useful overall when allColumnsStatement
	 * is set to false.
	 *
	 * @param modified the supposing entity that has differences against {@code unmodified} entity
	 * @param unmodified the "original" (freshly loaded from database ?) entity
	 * @param allColumnsStatement true if all columns must be in the SQL statement, false if only modified ones should be in
	 */
	default void update(C modified, C unmodified, boolean allColumnsStatement) {
		update(Collections.singletonList(new Duo<>(modified, unmodified)), allColumnsStatement);
	}
	
	/**
	 * Updates given entity in database according to following mechanism : it selects the existing data in database, then compares it with given
	 * entity in memory, and then updates database if necessary (nothing if no change was made).
	 *
	 * @param entity the entity to be updated
	 */
	default void update(C entity) {
		update(entity, true);
	}
	
	/**
	 * Updates given entity in database according to following mechanism : it selects the existing data in database, then compares it with given
	 * entity in memory, and then updates database if necessary (nothing if no change was made).
	 *
	 * @param entity the entity to be updated
	 * @param allColumnsStatement true to include all columns in statement even if only a part of them are touched, false to target only modified ones
	 */
	default void update(C entity, boolean allColumnsStatement) {
		update(entity, select(getId(entity)), allColumnsStatement);
	}
	
	/**
	 * Updates given entities in database according to following mechanism : it selects the existing data in database, then compares it with given
	 * entities in memory, and then updates database if necessary (nothing if no change was made).
	 * To be used for CRUD use case.
	 *
	 * @param entities the entities to be updated
	 */
	default void update(Iterable<C> entities) {
		// Below we keep order of given entities mainly to get steady unit tests. Meanwhile, this may have performance
		// impacts but very difficult to measure
		Set<I> ids = Iterables.collect(entities, this::getId, KeepOrderSet::new);
		Set<C> entitiesFromDb = select(ids);
		// Given entities may not be in same order than loaded ones from DB, whereas order is required for comparison (else everything is different !)
		// so we join them by their id to make them match
		Map<C, I> idPerEntity = Iterables.map(entities, Function.identity(), this::getId, KeepOrderMap::new);
		Map<I, C> entityFromDbPerId = Iterables.map(entitiesFromDb, this::getId, Function.identity());
		Map<C, C> modifiedVsUnmodifiedEntities = Maps.innerJoinOnValuesAndKeys(idPerEntity, entityFromDbPerId, KeepOrderMap::new);
		update(() -> new PairIterator<>(modifiedVsUnmodifiedEntities.keySet(), modifiedVsUnmodifiedEntities.values()), true);
	}
	
	/**
	 * Helping method for "Command Design Pattern" so one can apply modifications to the entity loaded by its id without any concern of loading it.
	 * This implementation will load twice the entity, invoke given {@link Consumer} with one of them, and then call {@link #update(Object, Object, boolean)} afterward.
	 * Subclasses may override this behavior to enhance loading or change its algorithm (by using {@link #updateById(Iterable)} for instance)
	 * 
	 * @param id key of entity to be modified 
	 * @param entityConsumer business code expected to modify its given entity
	 */
	@Experimental
	default void update(I id, Consumer<C> entityConsumer) {
		update(Collections.singleton(id), entityConsumer);
	}
	
	/**
	 * Massive version of {@link #update(Object, Consumer)}. {@link Consumer} will be called for each found entities.
	 * 
	 * @param ids keys of entities to be modified
	 * @param entityConsumer business code expected to modify its given entity
	 */
	@Experimental
	default void update(Iterable<I> ids, Consumer<C> entityConsumer) {
		Set<C> unmodified = select(ids);
		Set<C> modified = select(ids);
		modified.forEach(entityConsumer);
		update(() -> new PairIterator<>(modified, unmodified), true);
	}
	
	/**
	 * Deletes instances.
	 * Takes optimistic lock into account.
	 *
	 * @param entity entity to be deleted
	 * @throws StaleStateObjectException if deleted row count differs from the entities count
	 */
	default void delete(C entity) {
		delete(Collections.singletonList(entity));
	}
	
	/**
	 * Will delete instances only by their identifier.
	 * This method will not take optimistic lock (versioned entity) into account, so it will delete database rows "roughly".
	 *
	 * @param entity entity to be deleted
	 */
	default void deleteById(C entity) {
		deleteById(Collections.singletonList(entity));
	}
	
	default C select(I id) {
		return Iterables.first(select(Collections.singleton(id)));
	}

	default Set<C> select(I... ids) {
		return select(Arrays.asHashSet(ids));
	}
	
	/**
	 * Creates a query with some criteria based on some properties.
	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
	 * Raises an exception if the targeted property is not mapped as a persisted one (transient).
	 *
	 * @param getter a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableEntityQuery<C, ?> selectWhere(SerializableFunction<C, O> getter, ConditionalOperator<O, ?> operator) {
		return selectWhere(AccessorChain.fromMethodReference(getter), operator);
	}
	
	/**
	 * Creates a query with some criteria based on some properties.
	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param setter a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableEntityQuery<C, ?> selectWhere(SerializableBiConsumer<C, O> setter, ConditionalOperator<O, ?> operator) {
		return selectWhere(Arrays.asList(Accessors.mutatorByMethodReference(setter)), operator);
	}
	
	/**
	 * Variation of {@link #selectWhere(SerializableFunction, ConditionalOperator)} with a criteria on property of a property
	 * Please note that whole bean graph is loaded, not only entities that satisfy criteria.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param getter1 a property accessor
	 * @param getter2 a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O, A> ExecutableEntityQuery<C, ?> selectWhere(SerializableFunction<C, A> getter1, SerializableFunction<A, O> getter2, ConditionalOperator<O, ?> operator) {
		return selectWhere(AccessorChain.fromMethodReferences(getter1, getter2), operator);
	}
	
	/**
	 * Creates a query with some criteria based on some properties.
	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param accessorChain a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableEntityQuery<C, ?> selectWhere(List<? extends ValueAccessPoint<?>> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectWhere().and(accessorChain, operator);
	}
	
	/**
	 * Creates a query with some criteria based on some properties.
	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param accessorChain a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableEntityQuery<C, ?> selectWhere(AccessorChain<C, ?> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectWhere(accessorChain.getAccessors(), operator);
	}
	
	default <O> ExecutableEntityQuery<C, ?> selectWhere(CriteriaPath<C, ?> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectWhere(accessorChain.getAccessors(), operator);
	}
	
	default <O, S extends Collection<O>, NEXT> ExecutableEntityQuery<C, ?> selectWhere(SerializableCollectionFunction<C, S, O> accessor1, SerializableFunction<O, NEXT> accessor2, ConditionalOperator<NEXT, ?> operator) {
		return selectWhere(new CriteriaPath<>(accessor1).add(accessor2), operator);
	}
	
	/**
	 * Creates a query which criteria target mapped properties.
	 * Please note that the whole bean graph is loaded, not only entities that satisfy criteria.
	 *
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	ExecutableEntityQuery<C, ?> selectWhere();
	
	/**
	 * Creates a projection query which criteria target mapped properties.
	 * {@link Select} must be modified by given select adapter (by default all column that would allow to load the entity are present).
	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link org.codefilarete.stalactite.query.model.Selectable}
	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
	 * <strong>Note that all {@link org.codefilarete.stalactite.query.model.Selectable} added to the Select must have an alias</strong>.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param selectAdapter the {@link Select} clause modifier
	 * @param getter a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializableFunction<C, O> getter, ConditionalOperator<O, ?> operator) {
		return selectProjectionWhere(selectAdapter).and(getter, operator);
	}
	
	/**
	 * Creates a projection query which criteria target mapped properties.
	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link org.codefilarete.stalactite.query.model.Selectable}
	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
	 * <strong>Note that all {@link org.codefilarete.stalactite.query.model.Selectable} added to the Select must have an alias</strong>.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param selectAdapter the {@link Select} clause modifier
	 * @param setter a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializableBiConsumer<C, O> setter, ConditionalOperator<O, ?> operator) {
		return selectProjectionWhere(selectAdapter).and(setter, operator);
	}
	
	/**
	 * Creates a projection query which criteria target mapped properties.
	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link org.codefilarete.stalactite.query.model.Selectable}
	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
	 * <strong>Note that all {@link org.codefilarete.stalactite.query.model.Selectable} added to the Select must have an alias</strong>.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param selectAdapter the {@link Select} clause modifier
	 * @param getter1 a property accessor
	 * @param getter2 a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O, A> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializableFunction<C, A> getter1, SerializableFunction<A, O> getter2, ConditionalOperator<O, ?> operator) {
		return selectProjectionWhere(selectAdapter, AccessorChain.fromMethodReferences(getter1, getter2).getAccessors(), operator);
	}
	
	/**
	 * Creates a projection query which criteria target mapped properties.
	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link org.codefilarete.stalactite.query.model.Selectable}
	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
	 * <strong>Note that all {@link org.codefilarete.stalactite.query.model.Selectable} added to the Select must have an alias</strong>.
	 * Raises an exception if targeted property is not mapped as a persisted one (transient).
	 *
	 * @param selectAdapter the {@link Select} clause modifier
	 * @param accessorChain a property accessor
	 * @param operator criteria for the property
	 * @param <O> value type returned by property accessor
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, List<? extends ValueAccessPoint<?>> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectProjectionWhere(selectAdapter).and(accessorChain, operator);
	}
	
	default <O> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, CriteriaPath<C, ?> accessorChain, ConditionalOperator<O, ?> operator) {
		return selectProjectionWhere(selectAdapter, accessorChain.getAccessors(), operator);
	}
	
	default <O, S extends Collection<O>, NEXT> ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter, SerializableCollectionFunction<C, S, O> accessor1, SerializableFunction<O, NEXT> accessor2, ConditionalOperator<O, ?> operator) {
		return selectProjectionWhere(selectAdapter, new CriteriaPath<>(accessor1).add(accessor2), operator);
	}
	
	/**
	 * Creates a projection query which criteria target mapped properties.
	 * {@link Select} must be modified by given select adapter (by default all columns that would allow to load the entity are present).
	 * User is expected to modify default {@link Select} by clearing it (optional) and add its {@link org.codefilarete.stalactite.query.model.Selectable}
	 * ({@link org.codefilarete.stalactite.sql.ddl.structure.Column} or {@link org.codefilarete.stalactite.query.model.operator.SQLFunction}).
	 * Consumption and aggregation of query result is left to the user that must implement its {@link Accumulator}
	 * while executing the result of this method through {@link ExecutableProjection#execute(Accumulator)}.
	 * <strong>Note that all {@link org.codefilarete.stalactite.query.model.Selectable} added to the Select must have an alias</strong>.
	 *
	 * @param selectAdapter the {@link Select} clause modifier
	 * @return a {@link EntityCriteria} enhance to be executed through {@link ExecutableQuery#execute(Accumulator)}
	 */
	ExecutableProjectionQuery<C, ?> selectProjectionWhere(Consumer<SelectAdapter<C>> selectAdapter);
	
	Set<C> selectAll();
	
	boolean isNew(C entity);
	
	I getId(C entity);
	
	Class<C> getClassToPersist();
	
	/**
	 * Mashup between {@link EntityCriteria} and {@link ExecutableQuery} to make an {@link EntityCriteria} executable
	 * @param <C> type of object returned by query execution
	 */
	interface ExecutableEntityQuery<C, SELF extends ExecutableEntityQuery<C, SELF>> extends EntityCriteria<C, SELF>, ExecutableQuery<C>, FluentOrderByClause<C, SELF> {
		
		SELF set(String paramName, Object paramValue);
		
		/**
		 * Overridden for a more accurate return type.
		 * {@inheritDoc}
		 */
		ExecutableEntityQuery<C, SELF> beginNested();
		
		/**
		 * Overridden for a more accurate return type.
		 * {@inheritDoc}
		 */
		ExecutableEntityQuery<C, SELF> endNested();
		
	}
	
	/**
	 * Mashup between {@link EntityCriteria} and {@link ExecutableProjection} to make an {@link EntityCriteria} executable
	 * @param <C> type of object returned by query execution
	 */
	interface ExecutableProjectionQuery<C, SELF extends ExecutableProjectionQuery<C, SELF>> extends EntityCriteria<C, SELF>, ExecutableProjection, FluentOrderByClause<C, SELF> {
		
		SELF set(String paramName, Object paramValue);
		
		/**
		 * Overridden for a more accurate return type.
		 * {@inheritDoc}
		 */
		ExecutableProjectionQuery<C, SELF> beginNested();
		
		/**
		 * Overridden for a more accurate return type.
		 * {@inheritDoc}
		 */
		ExecutableProjectionQuery<C, SELF> endNested();
	}
	
	/**
	 * Abstraction to configure the select clause of a query.
	 *
	 * @param <C> the type of context or entity for which the selection is configured
	 */
	interface SelectAdapter<C> {
		
		Set<Selectable<?>> getColumns();
		
		SelectAdapter<C> distinct();
		
		SelectAdapter<C> setDistinct(boolean distinct);
		
		SelectAdapter<C> add(Selectable<?> column);
		
		SelectAdapter<C> add(Selectable<?> column, String alias);
		
		default SelectAdapter<C> add(CriteriaPath<C, ?> property) {
			return add(property.getAccessors());
		}
		
		default SelectAdapter<C> add(CriteriaPath<C, ?> property, String alias) {
			return add(property.getAccessors(), alias);
		}
		
		default SelectAdapter<C> add(List<ValueAccessPoint<?>> property) {
			return add(giveColumn(property));
		}
		
		default SelectAdapter<C> add(List<ValueAccessPoint<?>> property, String alias) {
			return this.add(giveColumn(property), alias);
		}
		
		default Selectable<?> giveColumn(ValueAccessPoint<?> property) {
			return giveColumn(Arrays.asList(property));
		}
		
		default Selectable<?> giveColumn(SerializableFunction<C, ?> property) {
			return giveColumn(Accessors.accessorByMethodReference(property));
		}
		
		Selectable<?> giveColumn(List<ValueAccessPoint<?>> property);
	}
}
